/*
 * Copyright (c) 2010, Jim Hollinger
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   * Neither the name of Jim Hollinger nor the names of its contributors
 *     may be used to endorse or promote products derived from this
 *     software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */

#include <string>
#include <vector>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "debughelper/debughelper.h"
#include "properties.h"


using namespace prop;


/**
 ****************************************************************************
 * @brief  Constructor that initializes the key/value pair
 *
 * @param  the_key    Initial key
 * @param  the_value  Initial value
 ****************************************************************************
**/
Properties::Property::Property(const std::string &the_key,
                               const std::string &the_value)
                               : key(the_key), value(the_value) {
    recognized_flag = false;
}


/**
 ****************************************************************************
 * @brief  Constructor that clears the properties list and
 *           initializes the comment, escape, and quote chars
 ****************************************************************************
**/
Properties::Properties() {
    properties.clear();
    comment_char = '#';
    escape_char = '\\';
    quote_char = '\"';
    filename.clear();
}

/**
 ****************************************************************************
 * @brief  Destructor that deletes all properties from the list
 ****************************************************************************
**/
Properties::~Properties() {
    while (properties.size() > 0) {
        delete properties.back();
        properties.pop_back();
    }
}

/**
 ****************************************************************************
 * @brief  Retrieve the value associated with the key
 *
 * @param  key  String to lookup
 *
 * @return String value associated with the key
 ****************************************************************************
**/
const std::string Properties::getProperty(const std::string &key) {
    std::string prop;

    bool found_flag = false;
    std::vector<Property *>::iterator property_iterator;
    for (property_iterator  = properties.begin();
         property_iterator != properties.end();
         ++property_iterator) {
        Property *property = *property_iterator;
        if (property->key == key) {
            prop = property->value;
            property->recognized_flag = true;
            found_flag = true;
            break;
        }
    }

    if (!found_flag) {
        add(key, "", true);
    }

    return prop;
}

/**
 ****************************************************************************
 * @brief  Retrieve the value associated with the key
 *
 * @param  key           String to lookup
 * @param  defaultValue  String returned if key is not found
 *
 * @return String value associated with the key or defaultValue
 ****************************************************************************
**/
const std::string Properties::getProperty(const std::string &key,
                                          const std::string &defaultValue) {
    std::string prop = getProperty(key);
    if (prop.empty()) {
        add(key, defaultValue, true);
        prop = defaultValue;
    }

    return prop;
}

/**
 ****************************************************************************
 * @brief  Retrieve the boolean value associated with the key
 *
 * @param  key           String to lookup
 * @param  defaultValue  String returned if key is not found
 *
 * @return Bool value associated with the key or defaultValue
 ****************************************************************************
**/
bool Properties::getBoolProperty(const std::string &key,
                                 const std::string &defaultValue) {
    bool value = false;

    std::string prop = getProperty(key, defaultValue);
    if ((prop == "1") ||
        (prop == "true") ||
        (prop == "True") ||
        (prop == "TRUE")) {
        value = true;
    } else if ((prop == "0") ||
               (prop == "false") ||
               (prop == "False") ||
               (prop == "FALSE")) {
        value = false;
    } else {
        fprintf(stderr,
            "Properties.getBoolProperty: Did not recognize value \"%s\" for Key(\"%s\")\n",
            prop.c_str(), key.c_str());
    }

    return value;
}

/**
 ****************************************************************************
 * @brief  Retrieve the boolean value associated with the key
 *
 * @param  key           String to lookup
 * @param  defaultValue  Bool returned if key is not found
 *
 * @return Bool value associated with the key or defaultValue
 ****************************************************************************
**/
bool Properties::getBoolProperty(const std::string &key, bool defaultValue) {
    bool value = getBoolProperty(key, std::string((defaultValue) ?
        "true" : "false"));

    return value;
}

/**
 ****************************************************************************
 * @brief  Retrieve the integer value associated with the key
 *
 * @param  key           String to lookup
 * @param  defaultValue  String returned if key is not found
 *
 * @return Long value associated with the key or defaultValue
 ****************************************************************************
**/
int Properties::getIntProperty(const std::string &key,
                               const std::string &defaultValue) {
    std::string prop = getProperty(key, defaultValue);
    int value = strtol(prop.c_str(), NULL, 10);

    return value;
}

/**
 ****************************************************************************
 * @brief  Retrieve the integer value associated with the key
 *
 * @param  key           String to lookup
 * @param  defaultValue  Long returned if key is not found
 *
 * @return Long value associated with the key or defaultValue
 ****************************************************************************
**/
int Properties::getIntProperty(const std::string &key, int defaultValue) {
    char s[64];
    sprintf(s, "%d", defaultValue);
    int value = getIntProperty(key, s);

    return value;
}

/**
 ****************************************************************************
 * @brief  Retrieve the floating point value associated with the key
 *
 * @param  key           String to lookup
 * @param  defaultValue  String returned if key is not found
 *
 * @return Float value associated with the key or defaultValue
 ****************************************************************************
**/
double Properties::getFloatProperty(const std::string &key,
                                    const std::string &defaultValue) {
    std::string prop = getProperty(key, defaultValue);
    double value = strtod(prop.c_str(), NULL);

    return value;
}

/**
 ****************************************************************************
 * @brief  Retrieve the floating point value associated with the key
 *
 * @param  key           String to lookup
 * @param  defaultValue  Float returned if key is not found
 *
 * @return Float value associated with the key or defaultValue
 ****************************************************************************
**/
double Properties::getFloatProperty(const std::string &key,
                                    double defaultValue) {
    char s[64];
    sprintf(s, "%f", defaultValue);
    double value = getFloatProperty(key, s);

    return value;
}

/**
 ****************************************************************************
 * @brief  Verify the value associated with the key
 *
 * @param  key            String to lookup
 * @param  expectedValue  String used for comparison
 *
 * @return True if value associated with the key equals expectedValue
 ****************************************************************************
**/
bool Properties::verifyProperty(const std::string &key,
                                const std::string &expectedValue) {
    bool flag = false;

    std::string prop = getProperty(key);
    if (prop == expectedValue) {
        flag = true;
    } else {
        fprintf(stderr,
            "Properties.verifyProperty: Key(\"%s\") [\"%s\" != \"%s\"]\n",
            key.c_str(), prop.c_str(), expectedValue.c_str());
        flag = false;
    }

    return flag;
}

/**
 ****************************************************************************
 * @brief  Outputs the key/value pairs
 *
 * @param  the_filename  Name of file to which output is written
 * @param  text      A string outputted as the first line (usually a comment)
 *
 * @return True if file could be written
 ****************************************************************************
**/
bool Properties::list(const std::string &the_filename,
                      const std::string &text) {
    bool flag = false;

    filename = the_filename;
    FILE *fp = fopen(filename.c_str(), "wt");
    if (fp != NULL) {
        flag = true;
        list(fp, text);
        fclose(fp);
        fp = NULL;
    }

    return flag;
}

/**
 ****************************************************************************
 * @brief  Outputs the key/value pairs
 *
 * @param  fp    FILE pointer to which output is written
 * @param  text  A string outputted as the first line (usually a comment)
 ****************************************************************************
**/
void Properties::list(FILE *fp, const std::string &text) {
    const char *s = text.c_str();
    fwrite(s, 1, strlen(s), fp);
    std::vector<Property *>::iterator property_iterator;
    for (property_iterator  = properties.begin();
         property_iterator != properties.end();
         ++property_iterator) {
        Property *property = *property_iterator;
        if (property->value.empty()) {
            fprintf(fp, "#%s\n", property->key.c_str());
        } else {
            fprintf(fp, "%s\t%s\n",
                property->key.c_str(), property->value.c_str());
        }
    }
}

/**
 ****************************************************************************
 * @brief  Outputs any key/value pairs that were not recognized
 *
 *  The class keeps track of the keys retrieved using the getProperty
 *    method. Any keys that are not retrieved are assumed to be
 *    unrecognized keys from the load operation. This can be usefull
 *    in identifying typos in the properties file loaded.
 * 
 * @param  fp    FILE pointer to which output is written
 * @param  text  A string outputted as the first line (usually an error message)
 ****************************************************************************
**/
bool Properties::listUnrecognized(FILE *fp, const std::string &text) {
    int numUnrecognized = 0;

    std::vector<Property *>::iterator property_iterator;
    for (property_iterator  = properties.begin();
         property_iterator != properties.end();
         ++property_iterator) {
        Property *property = *property_iterator;
        if (!property->recognized_flag) {
            if (numUnrecognized++ <= 0) {
                const char *s = text.c_str();
                fwrite(s, 1, strlen(s), fp);
            }
            if (property->value.empty()) {
                fprintf(fp, "\t#%s\n", property->key.c_str());
            } else {
                fprintf(fp, "\t%s\t%s\n",
                    property->key.c_str(), property->value.c_str());
            }
        }
    }

    return (numUnrecognized > 0);
}

/**
 ****************************************************************************
 * @brief  Inputs key/value pairs
 *
 * @param  the_filename  Name of file from which input is read
 *
 * @return True if file could be read
 ****************************************************************************
**/
bool Properties::load(const std::string &the_filename) {
    bool flag = false;

    filename = the_filename;
    FILE *fp = fopen(filename.c_str(), "rt");
    if (fp != NULL) {
        flag = true;
        load(fp);
        fclose(fp);
        fp = NULL;
    }

    return flag;
}

/**
 ****************************************************************************
 * @brief  Inputs key/value pairs
 *
 * @param  fp  FILE pointer from which input is read
 ****************************************************************************
**/
void Properties::load(FILE *fp) {
    char buf[1024];
    int  lineno = 0;

    bool continue_flag = true;
    while (continue_flag) {

        // get a line
        buf[sizeof(buf) - 1] = '\0';
        char *s = fgets(buf, sizeof(buf) - 1, fp);
        if (s == NULL) {
            continue_flag = false;  // EOF
        } else {
            int len = static_cast<int> (strlen(s));
            load(buf, len, ++lineno);
        }
    }
}

/**
 ****************************************************************************
 * @brief  Parses one line of text for a key/value pair
 *
 *  Key/value pairs consist of a key that begins with an alpha character and
 *   ends with the first white space. The corresponding value starts at the
 *   next non-white space character and extends to the end of the line.
 *   Leading and trailing white space is omitted.
 *   Key and value strings enclosed in quotes may conatin leading, trailing,
 *   and embedded white space.
 *   An escape character can be used to embed a quote character.
 *   All characters after the comment character to the end of the line are ignored.
 *
 * @param  buf     Char array containing one line of text
 * @param  len     Number of characters in line of text
 * @param  lineno  Corresponding line number from file, used in error reporting
 ****************************************************************************
**/
void Properties::load(char *buf, int len, int lineno) {
    char *key = NULL;
    char *value = NULL;
    int   c = '\0';
    int   i = 0;
    bool  foundName_flag     = false;
    bool  foundNameEnd_flag  = false;
    bool  foundValue_flag    = false;
    bool  foundValueEnd_flag = false;

    // find begining of key (alpha or quote)
    for (; i < len; i++) {
        c = static_cast<int> ((unsigned char) buf[i]);
        if ((c == '\0') || (c == '\r') || (c == '\n') || (c == comment_char)) {
            break;  // skip blank lines and comment lines
        } else if (isalpha(c) || (c == quote_char)) {
            key = &buf[i++];
            foundName_flag = true;
            break;
        } else if (!isspace(c)) {
            if (isprint(c)) {
                fprintf(stderr,
                    "Properties.load: Invalid character '%c' on line %d of \"%s\"\n",
                    c, lineno, filename.c_str());
            } else {
                fprintf(stderr,
                    "Properties.load: Invalid character '0x%02X' on line %d of \"%s\"\n",
                    c, lineno, filename.c_str());
            }
            break;
        }
    }

    // find end of key (white space)
    if (foundName_flag) {
        bool quotesClosed_flag = (c != '\"');
        for (; i < len; i++) {
            c = static_cast<int> ((unsigned char) buf[i]);
            if (c == escape_char) {
                memcpy(&buf[i], &buf[i] + 1, len - i);
            } else if (quotesClosed_flag) {
                if ((c == '\0') || (c == '\r') || (c == '\n')) {
                    buf[i++] = '\0';  // null terminate key
                    break;
                } else if (isspace(c)) {
                    buf[i++] = '\0';  // null terminate key
                    foundNameEnd_flag = true;
                    break;
                }
            } else {
                if ((c == '\0') || (c == '\r') || (c == '\n')) {
                    break;
                } else if (c == '\"') {
                    quotesClosed_flag = true;
                }
            }
        }

        // find begining of value (non-white space)
        if (foundNameEnd_flag) {
            for (; i < len; i++) {
                c = static_cast<int> ((unsigned char) buf[i]);
                if ((c == '\0') ||
                    (c == '\r') ||
                    (c == '\n') ||
                    (c == comment_char)) {
                    break;
                } else if (!isspace(c)) {
                    value = &buf[i++];
                    foundValue_flag = true;
                    break;
                }
            }

            // find end of value (end-of-line or comment)
            if (foundValue_flag) {
                bool quotesClosed_flag = (c != '\"');
                char *space = NULL;
                for (; i < len; i++) {
                    c = static_cast<int> ((unsigned char) buf[i]);
                    if (c == escape_char) {
                        memcpy(&buf[i], &buf[i] + 1, len - i);
                    } else if (quotesClosed_flag) {
                        if ((c == '\0') ||
                            (c == '\r') ||
                            (c == '\n') ||
                            (c == comment_char)) {
                            if (space == NULL) {
                                space = &buf[i];
                            }
                            *space = '\0';  // null terminate value and trim trailing white space
                            foundValueEnd_flag = true;
                            break;
                        } else if (isspace(c)) {
                            if (space == NULL) {
                                space = &buf[i];
                            }
                        } else {
                            space = NULL;
                        }
                    } else {
                        if ((c == '\0') || (c == '\r') || (c == '\n')) {
                            break;
                        } else if (c == '\"') {
                            quotesClosed_flag = true;
                        }
                    }
                }

                // got key/value pair
                if (foundValueEnd_flag) {

                    // remove key quotes if present
                    if (key[0] == '\"') {
                        int n = static_cast<int> (strlen(key));
                        if ((n > 0) && (key[n - 1] == '\"')) {
                            key[n - 1] = '\0';
                            key++;
                        }
                    }

                    // remove value quotes if present
                    if (value[0] == '\"') {
                        int n = static_cast<int> (strlen(value));
                        if ((n > 0) && (value[n - 1] == '\"')) {
                            value[n - 1] = '\0';
                            value++;
                        }
                    }

                    // add key/value pair to the properties list
                    add(key, value);
                } else {
                    fprintf(stderr,
                        "Properties.load: No closing quote on line %d of \"%s\"\n",
                        lineno, filename.c_str());
                }
            } else {
                fprintf(stderr,
                    "Properties.load: No value for \"%s\" on line %d of \"%s\"\n",
                    key, lineno, filename.c_str());
            }
        } else {
                fprintf(stderr,
                    "Properties.load: No value for \"%s\" on line %d of \"%s\"\n",
                    key, lineno, filename.c_str());
        }
    }
}

/**
 ****************************************************************************
 * @brief  Appends a key/value pair the the properties list
 *
 * @param  key    The key
 * @param  value  The value
 * @param  recognized_flag  Initial value for corresponding member of
 *                            new Property object (defaults to False)
 ****************************************************************************
**/
void Properties::add(const std::string &key,
                     const std::string &value,
                     bool recognized_flag) {
    bool found_flag = false;

    // check if property already exists
    std::vector<Property *>::iterator property_iterator;
    for (property_iterator  = properties.begin();
         property_iterator != properties.end();
         ++property_iterator) {
        Property *property = *property_iterator;

        // if the key already exists,
        // then overwrite the value with the new value
        if (property->key == key) {
            property->value = value;
            property->recognized_flag = recognized_flag;
            found_flag = true;
            break;
        }
    }

    // if property does not exist, then add it
    if (!found_flag) {
        Property *property = new Property(key, value);
        property->recognized_flag = recognized_flag;
        properties.push_back(property);
    }
}

/**
 ****************************************************************************
 * @brief  Performs a self test of this class
 *
 * @return True if all tests passed
 ****************************************************************************
**/
bool Properties::unitTest() {
    bool flag = true;

    const char *filename = "Properties_unitTest.txt";
    FILE *fp = fopen(filename, "wt");
    if (fp == NULL) {
        fprintf(stderr, "Properties.unitTest Could not write to \"%s\"\n",
            filename);
        flag = false;
    } else {

        // write out test properties
        fprintf(fp, "# %s\n", filename);
        fprintf(fp, "\t# This is a comment line\n");
        fprintf(fp, "one 1\n");
        fprintf(fp, "two \t2 # two\n");
        fprintf(fp, "three the number three\n");
        fprintf(fp, "four.4 4.4 \n");
        fprintf(fp, "\" five \" \" 5 \"  # quotes\n");
        fprintf(fp, "      six      6 with spaces      # spaces\n");
        fprintf(fp, "seven\\ oh\\\\seven 7\\\"\\0\\#7 # escapes\n");
        fclose(fp);

        // now load them in
        Properties properties;
        flag = properties.load(filename);
        if (flag) {
            if (!properties.verifyProperty("one", "1")) {
                flag = false;
            }
            if (!properties.verifyProperty("two", "2")) {
                flag = false;
            }
            if (!properties.verifyProperty("three", "the number three")) {
                flag = false;
            }
            if (!properties.verifyProperty("four.4", "4.4")) {
                flag = false;
            }
            if (!properties.verifyProperty(" five ", " 5 ")) {
                flag = false;
            }
            if (!properties.verifyProperty("six", "6 with spaces")) {
                flag = false;
            }
            if (!properties.verifyProperty("seven oh\\seven", "7\"0#7")) {
                flag = false;
            }
            if (properties.listUnrecognized(stderr,
                "Properties.unitTest Unrecognized properties:\n")) {
                flag = false;
            }
        } else {
            fprintf(stderr, "Properties.unitTest Could not load \"%s\"\n",
                filename);
        }
    }

    printf("Properties.unitTest %s\n", (flag) ? "Passed" : "Failed");

    return flag;
}
